#!/usr/bin/env perl
#
# Copyright (c) 2012-2013 Jeffrey M. Squyres.  All rights reserved.
#
# $COPYRIGHT$
# 
# Additional copyrights may follow
# 
# $HEADER$
#

package RubySlippers::Action::Nest;

use strict;
use RubySlippers::Messages;
use RubySlippers::Globals;
use RubySlippers::Config;

# Where the nest executable is located
my $_nest_exec;

# Number of times we'll try to run nest.py after a failure
my $_nest_retries = 3;

# Default values
my $_method_default = "soft";

# Nest config parameters
my $_nest_username;
my $_nest_password;
my $_method;
my $_testing;

# Current nest state
my $_nest_state;

# Will be set to empty if we're not testing
my $_testing_str = " (TESTING: no-op)";


#
# This function uses pynest to read the state of the Nest.  It parses
# the stdout from "nest.py show".
#
sub _read_nest_state {
    my $new_state;
    my $count = 1;

    while ($count <= $_nest_retries) {
        open(NEST, "$_nest_exec --user '$_nest_username' --password '$_nest_password' show|") || return 0;
        while (<NEST>) {
            chomp;
            my ($key, $value) = split(/: /, $_);
            $key =~ s/\.+$//;
            $new_state->{$key} = $value;
        }
        close(NEST);

        # Did we get anything back from nest.py?  (sometimes it
        # returns nothing / fails) If not, delay a little and try
        # again.
        last
            if (defined($new_state));

        Verbose("Nest: nest.py state query returned no output");
        ++$count;
        sleep(1);
    }

    # If we got something back, set the state and return happyness
    if (defined($new_state)) {
        %{$_nest_state} = %{$new_state};
        return 1;
    } else {
        # Sad panda
        return 0;
    }
}

#
# Set a nest state, and then confirm that it actually "took"
# (sometimes it won't work, so try a small number of times before
# giving up).
#
sub _run_nestpy {
    my $args = shift;
    my $check_field = shift;
    my $check_value = shift;

    my $count = 0;
    while ($count <= $_nest_retries) {
        my $ret = system("$_nest_exec --user '$_nest_username' --password '$_nest_password' $args");
        $ret = $ret >> 8;

        # If the system call failed, try again
        if (0 != $ret) {
            Verbose("Nest: Failed to run nest.py");
            ++$count;
            sleep(1);
            next;
        }

        # If we don't have a check field, we're done.
        last
            if (!defined($check_field));

        # Let that change propogate down to the Nest, then read its
        # state to verify that the change was actually set.
        sleep(1);
        if (_read_nest_state()) {
            if (lc($_nest_state->{$check_field}) eq lc($check_value)) {
                Verbose("Nest: Confirmed set \"$check_field\" to \"$check_value\"");
                return;
            }
            Verbose("Nest: State not set as expected: nest->$check_field = $_nest_state->{$check_field}, not $check_value");
        } else {
            Verbose("Nest: Unable to read current Nest state!");
        }

        # Didn't work, so sleep and try again
        sleep(1);
        ++$count;
    }

    Warning("Nest: Unable to set \"$check_field\" to \"$check_value\"!");
}

#
# This function calls pynest to set "home" or "away", but only if the
# current state of the Nest is not already set that way.
#
sub _set_nest_away {
    my $mode = shift;
    $mode = lc($mode);

    my $str;
    if ($mode eq "home") {
        if ($_nest_state->{away} eq "True") {
            # If we're setting "home", and the Nest says "away", then
            # set it.
            _run_nestpy("away here", "away", "false");
            $str = "Nest: Successfully set to to \"home\"";
        } else {
            # If we're setting "home", and the Nest already says "not
            # away", then don't set anything.
            $str = "Nest: Already set to \"home\" -- not doing anything";
        }
    } elsif ($mode eq "away") {
        if ($_nest_state->{away} eq "True") {
            # If we're setting "away", and the Nest already says
            # "away", then don't set anything.
            $str = "Nest: Already set to \"away\" -- not doing anything";
        } else {
            # If we're setting "away", and the Nest says "home", then
            # set it.
            _run_nestpy("away away", "away", "true");
            $str = "Nest: Successfully set to to \"away\"";
        }
    } else {
        Debug("Unknown mode: $mode -- ignored");
    }

    if (defined($str)) {
        Verbose($str);
    }
}


#
# This function calls pynest to enable or disable the auto away
# functionality, but only if the current state of the Nest is not
# already set that way.
#
sub _set_nest_auto_away {
    my $mode = shift;
    $mode = lc($mode);

    my $str;
    if ($mode eq "enable") {
        if ($_nest_state->{auto_away_enable} eq "True") {
            # If we're enabling auto-away and the Nest is already set
            # that way, then don't do anything.
            $str = "Nest: Auto-away already enabled -- not doing anything";
        } else {
            # If we're enabling auto-away and the Nest has it
            # disabled, then enable it.
            _run_nestpy("auto-away enable", "auto_away_enable", "true");
	    $str = "Nest: Successfully enabled auto-away (motion sensor)";
        }
    } elsif ($mode eq "disable") {
        if ($_nest_state->{auto_away_enable} eq "True") {
            # If we're disbling auto-away and the Nest has it enabled,
            # then disable it.
            _run_nestpy("auto-away disable", "auto_away_enable", "false");
	    $str = "Nest: Successfully disabled auto-away (motion sensor)";
        } else {
            # If we're enabling auto-away and the Nest is already set
            # that way, then don't do anything.
            $str = "Nest: Auto-away already disabled -- not doing anything";
        }
    } else {
        Debug("Unknown mode: $mode -- ignored");
    }

    if (defined($str)) {
        Verbose($str);
    }
}

#
# This function will be called by the Action engine with a state of
# HOME, NOT_HOME, or UNKNOWN.
#
sub _nest_act {
    my $state = shift;
    my $reason = shift;

    if (STATE_HOME != $state && STATE_NOT_HOME != $state) {
        Verbose("Nest: Got UNKNOWN state; not doing anything ($reason)");
        return;
    }

    # Get the current nest state.  Below, we'll only set something on
    # the Nest if it isn't already set that way.
    _read_nest_state();
    
    if (STATE_HOME == $state) {
        Verbose("Nest: Setting HOME ($_method)$_testing_str: $reason");
        if (!$_testing) {
            # Hard and soft are the same for the "home" state: set
            # "home" and disable auto-away functionality.
            _set_nest_away("home");
            _set_nest_auto_away("disable");
        }
    } elsif (STATE_NOT_HOME == $state) {
        Verbose("Nest: Setting AWAY ($_method)$_testing_str: $reason");
        if (!$_testing) {
            # For hard, set "away"
            if ($_method eq "hard") {
                _set_nest_away("away");
            }
            # For soft, just enable auto-away and let Nest determine
            # when we're gone
            else {
                _set_nest_auto_away("enable");
            }
        }
    }
}

#
# Open the module.  We qualify to return a module if we find nest.py
# in the path and also find a nest_username and nest_password in the
# config.
#
sub open {
    $_nest_username = RubySlippers::Config::get("nest_username");
    $_nest_password = RubySlippers::Config::get("nest_password");
    $_method = RubySlippers::Config::get("nest_away_method");
    $_testing = RubySlippers::Config::get("nest_testing");

    $_testing_str = ""
        if (!$_testing);

    if ((defined($_nest_username) && !defined($_nest_password)) ||
        (!defined($_nest_username) && defined($_nest_password))) {
        Warning("If either of nest_username or nest_password are defined, they must both be defined.  Skipping the Nest module.");
        return undef;
    }
    if (!defined($_nest_username) && !defined($_nest_password)) {
        Warning("Did not find either nest_username or nest_password; skipping the Nest module");
        return undef;
    }

    $_method = $_method_default
        if ($_method eq "");
    $_method = lc($_method);
    if ($_method ne "hard" && $_method ne "soft") {
        Warning("The nest_away_method must be blank/undefined, \"soft\", or \"hard\"; skipping the Nest module");
        return undef;
    }

    $_nest_exec = RubySlippers::Config::get("nest_bin");
    $_nest_exec = RubySlippers::Util::path_find("nest.py")
        if (!defined($_nest_exec));
    if (!defined($_nest_exec) || ! -x $_nest_exec) {
        Verbose("Did not find nest.py; skipping the Nest module");
        return undef;
    }
    
    Debug("Nest found $_nest_exec");

    Verbose("Reading Nest current state...");
    if (!_read_nest_state()) {
        Verbose("Unable to read the Nest state; skipping the Nest plugin");
        return undef;
    }

    Verbose("Nest returning Action module");
    Verbose("    Nest account:   $_nest_username");
    Verbose("    Away method:    $_method");
    Verbose("    Nest mode:       " .
            ($_nest_state->{away} eq "True" ? "Away" : "Home"));
    Verbose("    Nest auto away: " .
            ($_nest_state->{auto_away_enable} eq "True" ?
             "Enabled" : "Disabled") . "");
    Verbose("    Testing:      No commands will actually be sent to the Nest")
        if ($_testing);
    
    my @ret;
    push(@ret, {
        act => \&_nest_act,
        name => "Nest action",
         });
    return \@ret;
}

1;
